package lab.poc.erl;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;
import java.util.List;
import java.util.concurrent.CancellationException;

import static lab.poc.erl.Objects.hash;
import static lab.poc.erl.Objects.hashCombine;

public final class Pid implements Comparable<Pid> {

	public static final Pid UNDEFINED = new Pid();
	static final ThreadLocal<Pid> PID = new ThreadLocal<Pid>();

	private final String nodeName;
	private final int id;
	private final int serial;
	private final int creation;

	private int hCode_ = 0;

	private final Node node;
	private final Actor actor;
	private volatile Pid parentProcess;

	public static final Pid self() {
		final int MAX_REPEATS = 60;
		int retriesAttempted = 0;

		Pid pid = null;
		while (pid == null && ++retriesAttempted <= MAX_REPEATS) {
			int busySpin = 0;
			while (pid == null && ++busySpin <= 4000) {
				pid = PID.get();
				if (pid != null) {
					return pid;
				}
			}
			try {
				Thread.sleep(16L);
			} catch (InterruptedException e) {
				Thread.currentThread().interrupt();
			}
		}
		throw new IllegalThreadStateException("non-existent thread-local Pid!");
	}

	public final Object send(final Object message) {
		if (message != null /*&& actor.getClass() != MockProcess.class*/) {
			actor.send(message);
		}
		return message;
	}

	public final void exit() {
		actor.exit();
	}

	/**
	 * Establish a bidirectional link between this Pid and {@link otherPid}.
	 * @param otherPid the other Pid to link with.
	 */
	private boolean link(final Pid otherPid) {
		if (this != otherPid /*&& !this.isMockPid()*/) {
			if (actor.addToLinkSet(otherPid)) {
				return otherPid.link(this);
			}
		}
		return false;
	}

	final boolean unlink(final Pid otherPid) {
		if (this != otherPid) {
			if (actor.removeFromLinkSet(otherPid)) {
				return otherPid.unlink(this);
			}
		}
		return false;
	}

	final void signalExit(final Pid exitingPid) {
		unlink(exitingPid);
		exit();
	}

	final void clearParentProcess() {
		parentProcess = null;
	}

	final Pid parent() {
		return parentProcess;
	}

	private Pid() {
		this.nodeName = "undefined";
		this.serial = 0x2000;
		this.id = 0x8000;
		this.creation = 0x04;
		this.node = null;
		this.actor = null;
		this.parentProcess = null;
	}

	Pid(final Node node, final Class<? extends Actor> actorClass,
			final List<Object> args, final int id, final int serial,
			final int creation, final boolean linkToCreator) {
		this.node = node;
		this.nodeName = node.getName();
		this.id = id & 0x7fff; // 15 bits
		this.serial = serial & 0x1fff; // 13 bits
		this.creation = creation & 0x03; // 2 bits
		try {
			Constructor<? extends Actor> actorClassCons = actorClass
					.getConstructor();
			actorClassCons.setAccessible(true);
			actor = actorClassCons.newInstance();
			actor.setPid(this);
			actor.setNode(this.node);
		} catch (InstantiationException e) {
			throw new TypeNotPresentException(actorClass.getName(), e);
		} catch (InvocationTargetException e) {
			throw new RuntimeException(actorClass.getName(), e);
		} catch (NoSuchMethodException e) {
			throw new TypeNotPresentException(actorClass.getName() + ".<init>",
					e);
		} catch (IllegalAccessException e) {
			throw new SecurityException(actorClass.getName(), e);
		}

		if (actorClass == MockProcess.class) {
			this.parentProcess = UNDEFINED;
			// invoke on current thread
			actor.setThread(Thread.currentThread());
			actor.onSpawn(null);
			return;
		}
		Pid parent = Pid.self();
		this.parentProcess = parent;
		if (linkToCreator) {
			parent.link(this);
		}
		this.node.actorService.execute(new RunnableProcess() {
			@Override
			public void run() {
				actor.setThread(Thread.currentThread());
				try {
					actor.onSpawn(args);
					actor.notifyLinkedProcesses(Exit.NORMAL, null);
				} catch (CancellationException ce) {
					actor.notifyLinkedProcesses(Exit.CANCEL, ce);
					throw ce;
				} catch (RuntimeException re) {
					actor.notifyLinkedProcesses(Exit.ABNORMAL, re);
					throw re;
				} catch (Error er) {
					actor.notifyLinkedProcesses(Exit.ABNORMAL, er);
					throw er;
				} finally {
					actor.shutdown();
				}
			}

			@Override
			public Mailbox getMailbox() {
				return actor.getMailbox();
			}

			@Override
			public Pid getPid() {
				return Pid.this;
			}

			@Override
			public void dissolveFromThread() {
				actor.lockMailbox();
				actor.setThread(null);
			}
		});
	}

	final boolean isMockPid() {
		return MockProcess.class.equals(actor.getClass());
	}

	@Override
	public String toString() {
		if (this == UNDEFINED) {
			return "#Pid<" + nodeName + ">";
		}
		return "#Pid<" + nodeName + "." + id + "." + serial + ">";
	}

	@Override
	public int hashCode() {
		if (hCode_ == 0) {
			int hash = hashCombine(31, creation);
			hash = hashCombine(hash, serial);
			hash = hashCombine(hash, id);
			hCode_ = hashCombine(hash, hash(nodeName));
		}
		return hCode_;
	}

	@Override
	public boolean equals(final Object other) {
		if (other instanceof Pid) {
			final Pid otherPid = (Pid) other;
			return (nodeName.compareTo(otherPid.nodeName) == 0
					&& id == otherPid.id && serial == otherPid.serial && creation == otherPid.creation);
		}
		return false;
	}

	@Override
	public int compareTo(final Pid otherPid) {
		if (otherPid == null) {
			return -1;
		}
		if (creation == otherPid.creation) {
			if (serial == otherPid.serial) {
				if (id == otherPid.id) {
					return nodeName.compareTo(otherPid.nodeName);
				} else {
					return id - otherPid.id;
				}
			} else {
				return serial - otherPid.serial;
			}
		} else {
			return creation - otherPid.creation;
		}
	}
}
